package de.jabc.sib.bio.ebi;

import java.lang.reflect.Field;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import org.apache.log4j.Logger;

import de.metaframe.jabc.framework.sib.DocType;
import de.metaframe.jabc.framework.sib.SIB;
import de.metaframe.jabc.framework.sib.annotation.SIBClass;
import de.metaframe.jabc.framework.sib.annotation.SIBClassLoader;

/**
 * This utility class provides documentation for the SIBs. To avoid copy'n'paste
 * in the resource bundles, a key may link to the documentation of another key
 * using the special prefix "doc:" as illustrated below:
 * 
 * <pre>
 * key1 = doc:key2
 * key2 = This is a localized message.
 * </pre>
 * 
 * SIB libraries should typically create an instance of this class once per
 * entire library. This might be accomplished by storing the documentation
 * instance in a static field of some class (e.g. the superclass of all SIBs):
 * 
 * <pre>
 * protected static final Documentation strings = new Documentation(MySIB.class);
 * </pre>
 * 
 * @author Benjamin Bentmann
 * @version $Revision: 2576 $ $Date: 2008-01-13 12:01:43 +0100 (Sun, 13 Jan 2008) $
 */
@SIBClassLoader
public class Documentation {

	/**
	 * The logger for this class.
	 */
	private static Logger log = Logger.getLogger(Documentation.class);

	/**
	 * The class loader to use for looking up the resource bundles.
	 */
	private ClassLoader loader;

	/**
	 * The qualified base name of the resource bundle family.
	 */
	private String bundle;

	/**
	 * The locale to use for the SIB documentation.
	 */
	private Locale locale;

	/**
	 * Creates a new SIB documentation bundle for the specified class. The class
	 * loader of the given class object will be used to load the resource
	 * bundles. The qualified base name of the resource bundle family will be
	 * derived by appending ".i18n.sibs" to the package hosting the class
	 * object. For example, if the given class object is named
	 * "de.jabc.sib.MySIB" the base name will be "de.jabc.sib.i18n.sibs".
	 * 
	 * @param caller
	 *            The class object used to configure this instance, in most
	 *            cases the class of the caller.
	 * @throws NullPointerException
	 *             If the class argument is {@code null}.
	 */
	public Documentation(Class<?> caller) {
		this(caller.getClassLoader(), getBundleName(caller));
	}

	/**
	 * Gets the qualified base name of the resource bundle for the specified
	 * class.
	 * 
	 * @param caller
	 *            The class object used to derive the qualified base name for
	 *            the resource bundle family
	 * @return The qualified bundle base name.
	 * @throws NullPointerException
	 *             If the class argument is {@code null}.
	 */
	private static String getBundleName(Class<?> caller) {
		return caller.getPackage().getName() + ".i18n.sibs";
	}

	/**
	 * Creates a new SIB documentation bundle with the specified configuration.
	 * 
	 * @param loader
	 *            The class loader to use for bundle lookup.
	 * @param bundle
	 *            The qualified base name of the resource bundle family.
	 */
	protected Documentation(ClassLoader loader, String bundle) {
		this.loader = loader;
		this.bundle = bundle;
		String language = System.getProperty(SIB.LOCALE);
		if (language == null) {
			this.locale = Locale.getDefault();
		} else {
			this.locale = new Locale(language);
		}
		if (log.isDebugEnabled()) {
			log.debug("Created new documentation bundle for " + this.bundle
					+ "_" + this.locale + " (" + this.loader + ")");
		}
	}

	/**
	 * Gets the tooltip documentation for the specified SIB or one of its
	 * branches/parameters. This method is fail-safe, i.e. it silently ignores
	 * any exceptions during resource lookup.
	 * <p>
	 * The key to the documentation in the resource bundle is constructed
	 * according to the following pattern: {@code <sib-uid>.<category>}[{@code .<param>}]
	 * where all unsafe characters have been replaced by '_'. Mutable branches
	 * are an exception to this rule as their bundle key is always
	 * {@code <sib-uid>.branch_mutable}.
	 * 
	 * @param sib
	 *            The SIB whose documentation is to retrieve.
	 * @param category
	 *            The category of the documentation.
	 * @param param
	 *            The name of the branch/parameter to document.
	 * @return The requested documentation or {@code null} if no documentation
	 *         is available for the specified SIB (element).
	 */
	public String get(Object sib, DocType category, String param) {
		ResourceBundle bundle;
		try {
			bundle = getBundle();
		} catch (Exception e) {
			log.warn("Failed to load resource bundle", e);
			return e.toString();
		}
		String documentation = get(bundle, getKey(sib, category, param));
		documentation = toHtml(documentation, category, param);
		return documentation;
	}

	/**
	 * Gets a documentation string for the specified SIB, optionally replacing
	 * some text variables with the given arguments. The final bundle key used
	 * for the lookup will be of the form [{@code <sib-uid>}]{@code .<key>}.
	 * 
	 * @see MessageFormat
	 * 
	 * @param sib
	 *            The SIB whose documentation is to retrieve, may be
	 *            {@code null}.
	 * @param key
	 *            The key to use for the lookup.
	 * @param arguments
	 *            Some optional arguments that should be used to replace
	 *            variables in the documentation string, may be {@code null}.
	 * @return The requested documentation or {@code null} if no documentation
	 *         is available for the specified key.
	 */
	public String get(SIB sib, String key, Object... arguments) {
		ResourceBundle bundle;
		try {
			bundle = getBundle();
		} catch (Exception e) {
			log.warn("Failed to load resource bundle", e);
			return e.toString();
		}
		String documentation = get(bundle, getKey(sib.getObject(), key));
		if (documentation != null && arguments != null && arguments.length > 0) {
			try {
				MessageFormat formatter =
						new MessageFormat(documentation, bundle.getLocale());
				documentation = formatter.format(arguments);
			} catch (Exception e) {
				log.debug("Failed to expand bundle string arguments", e);
				// ignore, return string with unreplaced variables
			}
		}
		return documentation;
	}

	/**
	 * Gets a documentation string for the specified SIB, optionally replacing
	 * some text variables with the given arguments. This method is a special
	 * version of {@link #get(SIB, String, Object...)} that will return the
	 * specified bundle key rather than {@code null} in case the resource string
	 * was not found.
	 * 
	 * @param sib
	 *            The SIB whose documentation is to retrieve, may be
	 *            {@code null}.
	 * @param key
	 *            The key to use for the lookup.
	 * @param arguments
	 *            Some optional arguments that should be used to replace
	 *            variables in the documentation string, may be {@code null}.
	 * @return The requested documentation or the given {@code key} argument if
	 *         no documentation is available for the specified key.
	 */
	public String getNonNull(SIB sib, String key, Object... arguments) {
		String documentation = get(sib, key, arguments);
		return (documentation != null) ? documentation : key;
	}

	/**
	 * Gets the documentation for the specified key.
	 * 
	 * @param bundle
	 *            The resource bundle from which to lookup documentation.
	 * @param key
	 *            The key for which to retrieve documentation.
	 * @return The requested documentation or {@code null} if no documentation
	 *         is available for the specified key.
	 */
	protected String get(ResourceBundle bundle, String key) {
		String documentation = "";
		String resolvedKey = key;
		for (int i = 0; i < 16; i++) {
			try {
				documentation = bundle.getString(resolvedKey);
				if (!documentation.startsWith("doc:")) {
					break;
				}
				resolvedKey = documentation.substring(4).trim();
			} catch (MissingResourceException e) {
				log.debug("Missing documentation for key \"" + key + "\"");
				return null;
			}
		}
		return documentation;
	}

	/**
	 * Gets the resource bundle for the SIB documentation. Note that
	 * {@link ResourceBundle} already employs caching so we do not save a
	 * reference to the retrieved bundle which allows to garbage collect the
	 * bundle if it is no longer needed.
	 * 
	 * @return The resource bundle for the SIB documentation.
	 */
	protected ResourceBundle getBundle() {
		return ResourceBundle.getBundle(this.bundle, this.locale, this.loader);
	}

	/**
	 * Creates the bundle key for the specified SIB.
	 * 
	 * @param sib
	 *            The SIB for which documentation is requested, may be
	 *            {@code null}.
	 * @param key
	 *            The key that should be prefixed with the SIB base key.
	 * @return The bundle key used to lookup the documentation.
	 */
	protected String getKey(Object sib, String key) {
		StringBuilder buffer = new StringBuilder(256);
		if (sib != null) {
			buffer.append(replaceUnsafe(getUID(sib)));
			buffer.append('.');
		}
		buffer.append(key);
		return buffer.toString();
	}

	/**
	 * Creates the bundle key for the specified SIB element.
	 * 
	 * @param sib
	 *            The SIB for which documentation is requested.
	 * @param category
	 *            The kind of documentation being requested.
	 * @param param
	 *            The branch/parameter being documented.
	 * @return The bundle key used to lookup the documentation.
	 */
	protected String getKey(Object sib, DocType category, String param) {
		StringBuilder buffer = new StringBuilder(256);
		buffer.append(replaceUnsafe(getUID(sib)));
		buffer.append('.');
		if (category != null) {
			buffer.append(category.toString().toLowerCase(Locale.ENGLISH));
		}
		if (DocType.BRANCH.equals(category) && isMutableBranch(sib, param)) {
			buffer.append("_mutable");
		} else if (param != null) {
			buffer.append('.').append(replaceUnsafe(param));
		}
		return buffer.toString();
	}

	/**
	 * Gets the UID of the specified SIB.
	 * 
	 * @param sib
	 *            The SIB whose UID is to retrieve.
	 * @return The UID of the SIB.
	 */
	protected String getUID(Object sib) {
		SIBClass annotation = sib.getClass().getAnnotation(SIBClass.class);
		return annotation != null ? annotation.value() : "";
	}

	/**
	 * Tests whether the specified branch is a mutable branch of the given SIB.
	 * 
	 * @param sib
	 *            The SIB whose branches are to analyse.
	 * @param branch
	 *            The name of the branch to check.
	 * @return {@code true} if the specified branch is a mutable branch,
	 *         {@code false} otherwise.
	 */
	protected boolean isMutableBranch(Object sib, String branch) {
		try {
			Field field = sib.getClass().getField(SIB.FINAL_BRANCHES);
			String[] finalBranches = (String[]) field.get(sib);
			for (String finalBranch : finalBranches) {
				if (finalBranch.equals(branch))
					return false;
			}
			return true;
		} catch (Exception e) {
			return true;
		}
	}

	/**
	 * Replaces all unsafe characters in the specified property key.
	 * 
	 * @see java.util.Properties#load(java.io.InputStream)
	 * 
	 * @param key
	 *            The key whose unsafe characters should be replaced
	 * @return The input key with all unsafe characters being replaced
	 */
	protected String replaceUnsafe(String key) {
		return key.replaceAll("[:= ]", "_");
	}

	/**
	 * Wraps the given documentation text into a HTML tooltip string.
	 * 
	 * @param documentation
	 *            The raw documentation text, may be {@code null}.
	 * @param category
	 *            The kind of documentation being requested.
	 * @param param
	 *            The branch/parameter being documented.
	 * @return The HTML tooltip for the SIB documentation or an empty string if
	 *         no documentation was provided.
	 */
	protected String toHtml(String documentation, DocType category, String param) {
		if (documentation == null || documentation.length() <= 0) {
			return "";
		}
		StringBuilder buffer = new StringBuilder(documentation.length() + 64);
		buffer.append("<html><body>");
		if (param != null && param.length() > 0) {
			buffer.append("<b>");
			buffer.append(param);
			buffer.append(":</b><br>");
		}
		buffer.append(documentation);
		buffer.append("</body></html>");
		return buffer.toString();
	}

}
